// Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';

import 'package:analyzer/dart/analysis/utilities.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:build/build.dart';
import 'package:collection/collection.dart';

import 'module_library.dart';
import 'module_library_builder.dart';
import 'modules.dart';

/// An [Exception] that is thrown when a worker returns an error.
abstract class _WorkerException implements Exception {
  final AssetId failedAsset;

  final String error;

  /// A message to prepend to [toString] output.
  String get message;

  _WorkerException(this.failedAsset, this.error);

  @override
  String toString() => '$message:$failedAsset\n\nResponse:$error\n';
}

/// An [Exception] that is thrown when the analyzer fails to create a summary.
class AnalyzerSummaryException extends _WorkerException {
  @override
  final String message = 'Error creating summary for module';

  AnalyzerSummaryException(super.summaryId, super.error);
}

/// An [Exception] that is thrown when the common frontend fails to create a
/// kernel summary.
class KernelException extends _WorkerException {
  @override
  final String message = 'Error creating kernel summary for module';

  KernelException(super.summaryId, super.error);
}

/// An [Exception] that is thrown when there are some missing modules.
class MissingModulesException implements Exception {
  final String message;

  @override
  String toString() => message;

  MissingModulesException._(this.message);

  static Future<MissingModulesException> create(
    Set<AssetId> missingSources,
    Iterable<Module> transitiveModules,
    AssetReader reader,
  ) async {
    final buffer = StringBuffer('''
Unable to find modules for some sources, this is usually the result of either a
bad import, a missing dependency in a package (or possibly a dev_dependency
needs to move to a real dependency), or a build failure (if importing a
generated file).

Please check the following imports:\n
''');

    final checkedSourceDependencies = <AssetId, Set<AssetId>>{};
    for (final module in transitiveModules) {
      final missingIds = module.directDependencies.intersection(missingSources);
      for (final missingId in missingIds) {
        final checkedAlready = checkedSourceDependencies.putIfAbsent(
          missingId,
          () => <AssetId>{},
        );
        for (final sourceId in module.sources) {
          if (checkedAlready.contains(sourceId)) {
            continue;
          }
          checkedAlready.add(sourceId);
          final message = await _missingImportMessage(
            sourceId,
            missingId,
            reader,
          );
          if (message != null) buffer.writeln(message);
        }
      }
    }

    return MissingModulesException._(buffer.toString());
  }
}

/// Checks if [sourceId] directly imports [missingId], and returns an error
/// message if so.
Future<String?> _missingImportMessage(
  AssetId sourceId,
  AssetId missingId,
  AssetReader reader,
) async {
  final contents = await reader.readAsString(sourceId);
  final parsed = parseString(content: contents, throwIfDiagnostics: false).unit;
  final import = parsed.directives
      .whereType<UriBasedDirective>()
      .firstWhereOrNull((directive) {
        final uriString = directive.uri.stringValue;
        if (uriString == null) return false;
        if (uriString.startsWith('dart:')) return false;
        final id = AssetId.resolve(Uri.parse(uriString), from: sourceId);
        return id == missingId;
      });
  if (import == null) return null;
  final lineInfo = parsed.lineInfo.getLocation(import.offset);
  return '`$import` from $sourceId at $lineInfo';
}

/// An [Exception] that is thrown when there are some unsupported modules.
class UnsupportedModules implements Exception {
  final Set<Module> unsupportedModules;

  UnsupportedModules(this.unsupportedModules);

  Stream<ModuleLibrary> exactLibraries(AssetReader reader) async* {
    for (final module in unsupportedModules) {
      for (final source in module.sources) {
        final libraryId = source.changeExtension(moduleLibraryExtension);
        ModuleLibrary library;
        if (await reader.canRead(libraryId)) {
          library = ModuleLibrary.deserialize(
            libraryId,
            await reader.readAsString(libraryId),
          );
        } else {
          // A missing .module.library file indicates a part file, which can't
          // have import statements, so we just skip them.
          continue;
        }
        if (library.sdkDeps.any(
          (lib) => !module.platform.supportsLibrary(lib),
        )) {
          yield library;
        }
      }
    }
  }

  @override
  String toString() =>
      'Some modules contained libraries that were incompatible '
      'with the current platform.';
}
